# -*- coding:utf-8 -*-
__author__ = 'zhouyu'
__version__ = 'v0.1'

import copy
import pygame
import sys

# 导入地图数据库
from maps import *

''' 用元组方式来定义移动方向
    元素0：表示纵向步进
    元素1：表示横向步进 '''
DIR_U      = (-1, 0)    # 目标上移时位置下标变化
DIR_D      = (1, 0)     # 目标下移时位置下标变化
DIR_L      = (0, -1)    # 目标左移时位置下标变化
DIR_R      = (0, 1)     # 目标右移时位置下标变化

''' 事件响应返回值定义 '''
EVE_RESET  = -1         # 复位关卡
EVE_NONE   = 0          # 无任何处理
EVE_MOVE_O = 1          # 移动，分数-1
EVE_MOVE   = 2          # 移动，分数不变
EVE_MOVE_I = 3          # 移动，分数+1
EVE_UPDATE = 4          # 没有移动，但是形态改变


class Role(object):
    """ 角色类 """

    def __init__(self, x, y):
        self.x = x      # 角色的当前x轴坐标值
        self.y = y      # 角色的当前y轴坐标值

    @staticmethod
    def __form(curr, dest, mdir):
        """ 设置目标角色形态（只有向左和向右两种形态）

        Args:
            curr ：当前形态值
            dest ：目标元素值
            mdir ：方向值（x, y）

        Returns:
            返回新的形态值
        """

        ''' 向左移动 '''
        if mdir == DIR_L:

            ''' 移动目的地为空地 '''
            if dest == ELEM_FLOOR:
                return ELEM_USR_L

            ''' 移动目的地为target '''
            if dest == ELEM_TARGET:
                return ELEM_USR_L + ELEM_TARGET

            ''' 目标为墙壁或障碍物，需要判断当前是否为叠加状态 '''
            if dest == ELEM_WALL or dest == ELEM_BOX_F:
                return ELEM_USR_L + ELEM_TARGET if curr > ELEM_TARGET else ELEM_USR_L

        ''' 向右移动 '''
        if mdir == DIR_R:

            ''' 移动目的地为空地 '''
            if dest == ELEM_FLOOR:
                return ELEM_USR_R

            ''' 移动目的地为target '''
            if dest == ELEM_TARGET:
                return ELEM_USR_R + ELEM_TARGET

            ''' 移动目的地为墙壁或障碍物，需要判断当前是否为叠加状态 '''
            if dest == ELEM_WALL or dest == ELEM_BOX_F:
                return ELEM_USR_R + ELEM_TARGET if curr > ELEM_TARGET else ELEM_USR_R

        ''' 上下移动不改变主角形态，但可能会改变形态值 '''
        if mdir == DIR_U or mdir == DIR_D:

            ''' 移动目的地为墙壁或障碍物，不改变形态 '''
            if dest == ELEM_WALL or dest == ELEM_BOX_F:
                return curr

            ''' 由叠加形态切换为非叠加状态 '''
            if curr > ELEM_TARGET and dest == ELEM_FLOOR:
                return curr - ELEM_TARGET

            ''' 由非叠加状态切换为叠加状态 '''
            if curr < ELEM_TARGET and dest == ELEM_TARGET:
                return curr + ELEM_TARGET

        ''' 其他情况不改变主角形态值 '''
        return curr

    def move(self, data, mdir):
        """ 角色移动
        Args:
            data ：关卡地图数据（列表元组）
            mdir ：移动方向（是一个元组）

        Returns:
            True ：表示移动成功或图形发生变化，需要刷新地图
            False：表示未产生移动以及图形变化，地图不用刷新
        """

        ''' 获得目标位置的下标值 '''
        newx = self.x + mdir[1]
        newy = self.y + mdir[0]

        ''' 目标位置为墙壁或障碍物，不能移动 '''
        if data[newy][newx] == ELEM_WALL or data[newy][newx] == ELEM_BOX_F:

            ''' 虽然移不动，但允许改变角色形态 '''
            data[self.y][self.x] = self.__form(data[self.y][self.x], data[newy][newx], mdir)

            ''' 步数不增加 '''
            return EVE_UPDATE

        ''' 目标位置为空地 '''
        if data[newy][newx] == ELEM_FLOOR:

            ''' 设置角色形态 '''
            data[newy][newx] = self.__form(data[self.y][self.x], data[newy][newx], mdir)

            ''' 根据当前是否为叠加状态来判断还原值 '''
            data[self.y][self.x] = ELEM_TARGET if data[self.y][self.x] > ELEM_TARGET else ELEM_FLOOR

            ''' 记录新的位置 '''
            self.y = newy
            self.x = newx

            return EVE_MOVE

        ''' 目标位置为target '''
        if data[newy][newx] == ELEM_TARGET:

            ''' 设置角色形态 '''
            data[newy][newx] = self.__form(data[self.y][self.x], data[newy][newx], mdir)

            ''' 根据当前是叠加状态还是非叠加状态来决定还原值 '''
            data[self.y][self.x] = ELEM_TARGET if data[self.y][self.x] > ELEM_TARGET else ELEM_FLOOR

            ''' 记录新的位置 '''
            self.y = newy
            self.x = newx

            return EVE_MOVE

        ''' 目标位置为箱子（或已经在目标里的箱子） '''
        if data[newy][newx] == ELEM_BOX_I or data[newy][newx] == ELEM_BOX_O:

            ''' 创建箱子对象 '''
            box = Box(newx, newy)

            ''' 先尝试移动箱子，只有箱子能动了角色才能填充箱子的位置 '''
            ret = box.move(data, mdir)
            if ret is EVE_NONE:
                return EVE_NONE

            ''' 迭代调用，移动角色 '''
            self.move(data, mdir)

            return ret

        return EVE_NONE


class Box(object):
    """ 箱子类 """

    def __init__(self, x, y):
        self.x = x      # 箱子的当前x轴坐标值
        self.y = y      # 箱子的当前y轴坐标值

    def move(self, data, mdir):
        """ 箱子移动

        Args:
            data ：关卡地图数据（列表元组）
            mdir ：移动方向（是一个元组）

        Returns:
            True ：表示移动成功或图形发生变化，需要刷新地图
            False：表示未产生移动以及图形变化，地图不用刷新
        """

        ''' 获得目标位置的下标值 '''
        newx = self.x + mdir[1]
        newy = self.y + mdir[0]

        ''' 只允许目标位置为空地或者target '''
        if data[newy][newx] != ELEM_FLOOR and data[newy][newx] != ELEM_TARGET:
            return EVE_NONE

        ret = EVE_MOVE

        ''' 从target移除，分数减一 '''
        if data[self.y][self.x] == ELEM_BOX_I and data[newy][newx] == ELEM_FLOOR:
            ret = EVE_MOVE_O

        ''' 从非target移进target，分数加一 '''
        if data[self.y][self.x] == ELEM_BOX_O and data[newy][newx] == ELEM_TARGET:
            ret = EVE_MOVE_I

        ''' 设置目标箱子形态 '''
        data[newy][newx] = ELEM_BOX_O if data[newy][newx] == ELEM_FLOOR else ELEM_BOX_I

        ''' 还原当前位置 '''
        data[self.y][self.x] = ELEM_TARGET if data[self.y][self.x] == ELEM_BOX_I else ELEM_FLOOR

        return ret


class Round(object):
    """ 游戏关卡类 """

    def __init__(self, rnd_id, background=(0, 0, 0)):
        self.__color    = background
        self.__rnd_id   = rnd_id  # 关卡ID
        self.__mdata    = None    # 关卡地图数据

        self.__role     = None    # 角色对象
        self.__goals    = 0       # 得分数（一个箱子进入一个目标记一分）
        self.__targets  = 0       # 目标数（当得分数等于目标数，游戏过关）
        self.__m_pos    = [0, 0]  # 关卡地图在游戏地图中的起始位置

        self.__steps    = 0       # 步数统计

    def __load(self):
        """ 加载关卡地图，从地图中提取初始信息 """

        height = len(self.__mdata)
        width  = len(self.__mdata[0])
        box_cnt = 0

        if height > Window.height - 1 or width > Window.width:
            print("maps too big!")
            return False

        ''' 复位分数和目标的值 '''
        self.__goals   = 0
        self.__targets = 0

        ''' 根据地图大小计算地图坐标（让关卡地图居中显示） '''
        self.__m_pos[0] = (Window.height - height) // 2
        self.__m_pos[1] = (Window.width  - width ) // 2

        ''' 遍历关卡地图（注意坐标与数据下标的关系） '''
        for y in range(len(self.__mdata)):
            for x in range(len(self.__mdata[y])):

                # 计算箱子总数
                if self.__mdata[y][x] == ELEM_BOX_I or self.__mdata[y][x] == ELEM_BOX_O:
                    box_cnt += 1

                # 计算目标总数，允许游戏载入就有箱子落在目标里
                if self.__mdata[y][x] == ELEM_TARGET:
                    self.__targets += 1
                    continue

                # 计算起始得分数
                if self.__mdata[y][x] == ELEM_BOX_I:
                    self.__goals   += 1
                    self.__targets += 1
                    continue

                # 找角色当前位置
                if self.__mdata[y][x] != ELEM_USR_L and self.__mdata[y][x] != ELEM_USR_R:
                    continue

                # 创建角色，并记录它的当前坐标
                self.__role = Role(x, y)

        ''' 箱子总数必须等于target总数，否则地图不合法 '''
        if box_cnt != self.__targets:
            return False

        ''' 调试打印 '''
        print("=======level %02d=======" % self.__rnd_id)
        print("goals  :", self.__goals)
        print("targets:", self.__targets)
        print("m-pos  :", self.__m_pos)
        print("======================\n")

        return True

    def __show(self):
        """ 关卡信息显示 """

        ''' 文字显示与关卡地图左侧对齐，并置于关卡地图上一行 '''
        x = self.__m_pos[1]
        y = self.__m_pos[0] - 1

        ''' 关卡从1开始计数 '''
        info = "第 %d 关      步数:%d" % (self.__rnd_id + 1, self.__steps)

        font = pygame.font.SysFont("SimHei", 20, False)
        surf = font.render(info, True, (255, 255, 255))
        pygame.display.update(Window.screen.blit(surf, (x * 50, y * 50)))

        return True

    @staticmethod
    def __win():
        """ 关卡胜利提示及处理 """

        ''' 文字显示与关卡地图左侧对齐，并置于关卡地图上一行 '''
        x = 2
        y = Window.height // 2

        info = "恭喜你通关！敲任意键继续游戏"

        font = pygame.font.SysFont("kaiti", 45, False)
        surf = font.render(info, True, (255, 0, 0), (255, 255, 0))
        pygame.display.update(Window.screen.blit(surf, (x * 50, y * 50)))

        ''' 等待用户敲任意键进入下一关 '''
        while True:

            ''' 监控游戏事件（阻塞模式） '''
            event = pygame.event.wait()

            ''' QUIT事件，退出游戏 '''
            if event.type == pygame.QUIT:
                sys.exit()

            ''' QUIT事件，退出游戏 '''
            if event.type == pygame.KEYDOWN:
                break

        return True

    def __event(self):
        """ 事件处理 """

        ''' 监控游戏事件（阻塞模式） '''
        event = pygame.event.wait()

        ''' QUIT事件，退出游戏 '''
        if event.type == pygame.QUIT:
            sys.exit()

        ''' 暂时不处理非键盘事件（例如鼠标移动） '''
        if event.type != pygame.KEYDOWN:
            return EVE_NONE

        ''' 注意地图上的xy坐标与列表下标的关系（一位对应y，二维对应x） '''
        if event.key == pygame.K_UP:
            return self.__role.move(self.__mdata, DIR_U)

        if event.key == pygame.K_DOWN:
            return self.__role.move(self.__mdata, DIR_D)

        if event.key == pygame.K_LEFT:
            return self.__role.move(self.__mdata, DIR_L)

        if event.key == pygame.K_RIGHT:
            return self.__role.move(self.__mdata, DIR_R)

        ''' 支持ESC键重新开始本关卡 '''
        if event.key == pygame.K_ESCAPE:
            return EVE_RESET

        return EVE_NONE

    def proc(self):
        """ 关卡处理主程序 """

        ''' 复制地图，避免直接修改原始地图数据 '''
        self.__mdata = copy.deepcopy(MAPS[self.__rnd_id])

        ''' 加载关卡地图 '''
        if self.__load() is not True:
            print("maps load fail!")
            return False

        ''' 绘制游戏地图 '''
        Window.screen.fill(self.__color)
        Window.draw(self.__mdata, self.__m_pos)
        pygame.display.update()

        ''' 显示关卡信息 '''
        self.__show()

        ''' 游戏关卡内循环（持续监控并处理主角按键事件） '''
        while True:

            ''' 侦听游戏事件（阻塞模式） '''
            ret = self.__event()
            if ret == EVE_RESET:
                break

            ''' 返回EVE_NONE表示控制无效，无需刷新游戏界面，等待下一轮动作 '''
            if ret is EVE_NONE:
                continue

            ''' 增加步数 '''
            if ret is not EVE_UPDATE:
                self.__steps += 1

            ''' 刷新分数 '''
            if ret is EVE_MOVE_O:
                self.__goals -= 1

            if ret is EVE_MOVE_I:
                self.__goals += 1

            ''' 调试打印 '''
            print("----------------------")
            print("r-pos: [%d, %d]" % (self.__role.x, self.__role.y))
            print("goals:" , self.__goals)
            print("steps:" , self.__steps)
            print("----------------------\n")

            ''' 刷新游戏地图 '''
            Window.screen.fill(self.__color)
            Window.draw(self.__mdata, self.__m_pos)
            pygame.display.update()

            ''' 刷新关卡信息 '''
            self.__show()

            ''' 判断关卡是否完成，完成则退出关卡内循环（进入下一关卡） '''
            if self.__goals == self.__targets:
                break

        ''' 复位本关卡 '''
        if ret == EVE_RESET:
            del self.__mdata
            return self.proc()

        ''' 关卡完成 '''
        return self.__win()


class Window(object):
    """ 游戏窗口类 """

    ''' 地图大小 '''
    width   = 0
    height  = 0
    block   = 0

    ''' 屏幕对象 '''
    screen  = None

    ''' 游戏元素 '''
    role_l  = None      # 左向角色
    role_r  = None      # 右向角色
    box_in  = None      # 目标里的箱子
    box_out = None      # 目标外的箱子
    box_fix = None      # 障碍物（固定的箱子）
    target  = None      # 目标位置
    wall    = None      # 墙壁
    floor   = None      # 空地

    @classmethod
    def init(cls, headline, width, height, block):
        """ 游戏初始化 """

        ''' 记录窗口大小 '''
        cls.width  = width
        cls.height = height
        cls.block  = block

        ''' pygame初始化 '''
        pygame.init()

        ''' 加载并设置icon，读者可自行放入icon图标 '''
        icon = pygame.image.load(sys.path[0] + "./images/icon.png")
        pygame.display.set_icon(icon)

        ''' 设置游戏标题 '''
        pygame.display.set_caption(headline)

        ''' 创建游戏界面 '''
        cls.screen = pygame.display.set_mode((cls.width*cls.block, cls.height*cls.block))

        ''' 载入游戏资源（采用相对路径） '''
        cls.role_l  = pygame.image.load(sys.path[0] + "./images/lpython.png")
        cls.role_r  = pygame.image.load(sys.path[0] + "./images/rpython.png")
        cls.box_in  = pygame.image.load(sys.path[0] + "./images/box-in.png" )
        cls.box_out = pygame.image.load(sys.path[0] + "./images/box-out.png")
        cls.box_fix = pygame.image.load(sys.path[0] + "./images/box-fix.png")
        cls.target  = pygame.image.load(sys.path[0] + "./images/target.png" )
        cls.wall    = pygame.image.load(sys.path[0] + "./images/wall.png"   )
        cls.floor   = pygame.image.load(sys.path[0] + "./images/floor.png"  )

        return True

    @classmethod
    def draw(cls, data, pos):
        """ 画关卡地图

        Args:
            data ：关卡地图数据（二维列表）
            pos  ：关卡地图在游戏窗口中的起始坐标（tuple）

        Returns:
            True ：绘制成功
            False：绘制失败
        """

        ''' 遍历关卡地图 '''
        for y in range(len(data)):
            for x in range(len(data[y])):

                ''' 加上地图偏移坐标 '''
                xo = x + pos[1]
                yo = y + pos[0]

                # 画空地
                if data[y][x] == ELEM_FLOOR:
                    cls.screen.blit(cls.floor, (xo * cls.block, yo * cls.block))

                # 画墙壁
                elif data[y][x] == ELEM_WALL:
                    cls.screen.blit(cls.wall, (xo * cls.block, yo * cls.block))

                # 画障碍物（固定的箱子）
                elif data[y][x] == ELEM_BOX_F:
                    cls.screen.blit(cls.box_fix, (xo * cls.block, yo * cls.block))

                # 画目标外的箱子
                elif data[y][x] == ELEM_BOX_O:
                    cls.screen.blit(cls.box_out, (xo * cls.block, yo * cls.block))

                # 画目标内的箱子
                elif data[y][x] == ELEM_BOX_I:
                    cls.screen.blit(cls.box_in, (xo * cls.block, yo * cls.block))

                # 画左向主角（主角站在地板上和站在目标里数值不一样但图标一样）
                elif data[y][x] == ELEM_USR_L or data[y][x] == ELEM_USR_L + ELEM_TARGET:
                    cls.screen.blit(cls.role_l, (xo * cls.block, yo * cls.block))

                # 画右向主角（主角站在地板上和站在目标里数值不一样但图标一样）
                elif data[y][x] == ELEM_USR_R or data[y][x] == ELEM_USR_R + ELEM_TARGET:
                    cls.screen.blit(cls.role_r, (xo * cls.block, yo * cls.block))

                # 画目标位置
                elif data[y][x] == ELEM_TARGET:
                    cls.screen.blit(cls.target, (xo * cls.block, yo * cls.block))

        return True
